// Copyright (c) 2025 Shenzhen Kaihong Digital Industry Development Co., Ltd.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

mod list_available;
mod list_installed;
mod list_deployed;
mod dump_pkg_stat;
mod dump_deploy_stat;
mod show_hash;
mod init;
mod chroot;
mod install;
mod uninstall;
mod cleanup;
mod deploy;
mod undeploy;
mod rollback;

use std::{path::Path, str::FromStr};

use anyhow::Context;
use clap::{Parser, Subcommand};
use indicatif::MultiProgress;

use crate::{commands::{chroot::ChrootCommand, cleanup::CleanupCommand, deploy::DeployCommand, dump_deploy_stat::DumpDeployStatCommand, dump_pkg_stat::DumpPkgStatCommand, init::InitCommand, install::InstallCommand, list_available::ListAvailableCommand, list_deployed::ListDeployedCommand, list_installed::ListInstalledCommand, rollback::RollbackCommand, show_hash::ShowHashCommand, undeploy::UndeployCommand, uninstall::UninstallCommand}, config::{system::{toolchain::ToolchainConfig, PreloadSystemConfig, SystemConfig}, SpecsConfig}, database::{local::{CONFIG_ROOT, SYSTEM_CONFIG_PATH}, pkg_stat::StatSpec}, graph::{build_dep_graph::DepGraph, stat_dep_graph, PkgConfigResolver, PkgStatResolver}, PkgSpec};

pub trait FerriumCommand {
    #[allow(async_fn_in_trait)]
    async fn run(self, multi_progress: MultiProgress) -> anyhow::Result<()>;
}

#[derive(Debug, Subcommand)]
pub enum Commands {
    /// List all available packages
    ListAvailable(ListAvailableCommand),
    /// List all installed packages
    ListInstalled(ListInstalledCommand),
    /// List all deployed directories
    ListDeployed(ListDeployedCommand),
    /// Dump pkg stat file
    DumpPkgStat(DumpPkgStatCommand),
    /// Dump deploy stat file
    DumpDeployStat(DumpDeployStatCommand),
    /// Show the hash spec string of a package
    ShowHash(ShowHashCommand),
    /// Initialize the system config
    Init(InitCommand),
    /// Chroot into a deployed root
    Chroot(ChrootCommand),
    /// Install packages
    Install(InstallCommand),
    /// Uninstall packages
    Uninstall(UninstallCommand),
    /// Cleanup packages
    Cleanup(CleanupCommand),
    /// Deploy packages
    Deploy(DeployCommand),
    /// Undeploy packages
    Undeploy(UndeployCommand),
    /// Rollback deploy stat
    Rollback(RollbackCommand),
}

#[derive(Debug, Parser)]
pub struct BuildGraphArgs {
    /// Pkg spec strings or specs config paths
    specs: Vec<String>,
    /// Specify the base toolchain
    #[arg(long)]
    toolchain: Option<String>,
}

pub struct BuildGraphArgsRef<'a, I>
where I: Iterator<Item = &'a str>
{
    specs: I,
    toolchain: Option<String>,
}

impl BuildGraphArgs {
    pub async fn build_graph(self) -> anyhow::Result<(DepGraph, String, SystemConfig)> {
        BuildGraphArgsRef {
            specs: self.specs.iter().map(|s| s.as_str()),
            toolchain: self.toolchain,
        }.build_graph().await
    }
}

impl<'a, I> BuildGraphArgsRef<'a, I>
where I: Iterator<Item = &'a str>
{
    pub async fn build_graph(self) -> anyhow::Result<(DepGraph, String, SystemConfig)> {
        fn prepare_toolchain_name(toolchain: Option<String>, system_config: &SystemConfig) -> String {
            if let Some(toolchain) = toolchain {
                toolchain
            } else {
                system_config.default_toolchain.to_string()
            }
        }

        fn prepare_toolchain_config<'a>(
            system_config: &'a mut SystemConfig,
            toolchain: &str,
        ) -> anyhow::Result<&'a mut ToolchainConfig> {
            let toolchain_config = system_config.toolchains.0.get_mut(toolchain)
                .context(format!("toolchain '{}' not exists in config", toolchain))?;
            toolchain_config.sort_features();
            Ok(toolchain_config)
        }

        let specs = load_specs(self.specs).await?;

        let mut system_config = load_system_config().await?;
        let toolchain_name = prepare_toolchain_name(self.toolchain, &system_config);
        let toolchain_config = prepare_toolchain_config(&mut system_config, &toolchain_name)?;

        let mut resolver = PkgConfigResolver::new(toolchain_config);
        resolver.build_graph(specs.into_iter())
            .await
            .context("failed to resolve specs")?;
        Ok((resolver.dep_graph, toolchain_name, system_config))
    }
}

fn split_stat_specs(specs: Vec<String>) -> (Vec<String>, Vec<String>) {
    let mut pkg_specs = Vec::new();
    let mut stat_specs = Vec::new();
    for spec in specs {
        if StatSpec::from_str(&spec).is_ok() {
            stat_specs.push(spec);
        } else {
            pkg_specs.push(spec);
        }
    }

    (pkg_specs, stat_specs)
}

async fn load_system_config() -> anyhow::Result<SystemConfig> {
    let mut content = tokio::fs::read_to_string(SYSTEM_CONFIG_PATH.as_str()).await
        .context(format!("failed to read system config '{}'", SYSTEM_CONFIG_PATH.as_str()))?;
    let preload_config: PreloadSystemConfig = toml::from_str(&content)
        .context(format!("failed to parse system config '{}'", SYSTEM_CONFIG_PATH.as_str()))?;
    if let Some(includes) = preload_config.includes.as_ref() {
        for include in includes {
            let include_path = Path::new(CONFIG_ROOT.as_str()).join(include);
            let include_content = tokio::fs::read_to_string(&include_path)
                .await
                .context(format!("failed to read include config '{}'", include_path.display()))?;
            content.push('\n');
            content.push_str(&include_content);
        }
    }
    let config: SystemConfig = toml::from_str(&content)
        .context(format!("failed to parse system config '{}' with includes", SYSTEM_CONFIG_PATH.as_str()))?;
    config
        .verify()
        .context(format!("failed to verify system config '{}'", SYSTEM_CONFIG_PATH.as_str()))?;
    Ok(config)
}

async fn load_specs<'a, I>(spec_strs: I) -> anyhow::Result<Vec<PkgSpec>>
where I: Iterator<Item = &'a str>
{
    async fn load_specs_by_spec_str(specs: &mut Vec<PkgSpec>, spec_str: &str) -> anyhow::Result<()> {
        if spec_str.starts_with("/") || spec_str.starts_with(".") {
            let path = Path::new(&spec_str);
            let config = load_specs_config(path).await?;
            specs.extend(config.into_pkg_spec_iter());
        } else {
            let spec = PkgSpec::from_str(spec_str)
                .context(format!("invalid pkg spec string '{}'", spec_str))?;
            specs.push(spec);
        }
        Ok(())
    }

    async fn load_specs_config(path: &Path) -> anyhow::Result<SpecsConfig> {
        let content = tokio::fs::read_to_string(path).await
            .context(format!("failed to read specs config '{}'", path.display()))?;
        let config: SpecsConfig = toml::from_str(&content)
            .context(format!("failed to parse specs config '{}'", path.display()))?;
        Ok(config)
    }

    let mut specs = Vec::new();
    for spec_str in spec_strs {
        load_specs_by_spec_str(&mut specs, spec_str).await?;
    }
    Ok(specs)
}

async fn resolve_stats<'a, I>(specs: I) -> anyhow::Result<stat_dep_graph::DepGraph>
where I: Iterator<Item = &'a str>,
{
    let mut resolver = PkgStatResolver::default();
    resolver.build_graph(specs).await
        .context("failed to resolve stats")?;
    Ok(resolver.dep_graph)
}
